/*
 * UAE - The Un*x Amiga Emulator
 *
 * lowlevel cd device glue, scsi emulator
 *
 * Copyright 2009-2010 Toni Wilen
 *
 */

#include "sysconfig.h"
#include "blkdev.h"
#include "options.h"
#include "crc32.h"
#include "execio.h"
#include "savestate.h"
#include "thread.h"
#include "newcpu.h"
#include "traps.h"
#include "scsidev.h"

#define PRE_INSERT_DELAY (3 * (g_curr_conf.ntscmode ? 60 : 50))

static int scsiemu[MAX_TOTAL_SCSI_DEVICES];

static struct device_functions* device_func[MAX_TOTAL_SCSI_DEVICES];

static int openlist[MAX_TOTAL_SCSI_DEVICES];
static int waspaused[MAX_TOTAL_SCSI_DEVICES];
static int delayed[MAX_TOTAL_SCSI_DEVICES];
static uae_sem_t unitsem[MAX_TOTAL_SCSI_DEVICES];
static int unitsem_cnt[MAX_TOTAL_SCSI_DEVICES];

static int play_end_pos[MAX_TOTAL_SCSI_DEVICES];
static byte play_qcode[MAX_TOTAL_SCSI_DEVICES][SUBQ_SIZE];

static TCHAR newimagefiles[MAX_TOTAL_SCSI_DEVICES][256];
static int imagechangetime[MAX_TOTAL_SCSI_DEVICES];
static bool cdimagefileinuse[MAX_TOTAL_SCSI_DEVICES], wasopen[MAX_TOTAL_SCSI_DEVICES];

/* convert minutes, seconds and frames -> logical sector number */
int msf2lsn(int msf)
{
    int sector = (((msf >> 16) & 0xff) * 60 * 75 + ((msf >> 8) & 0xff) * 75 + ((msf >> 0) & 0xff));
    sector -= 150;
    return sector;
}

/* convert logical sector number -> minutes, seconds and frames */
int lsn2msf(int sectors)
{
    int msf;
    sectors += 150;
    msf = (sectors / (75 * 60)) << 16;
    msf |= ((sectors / 75) % 60) << 8;
    msf |= (sectors % 75) << 0;
    return msf;
}

byte frombcd(byte v)
{
    return (v >> 4) * 10 + (v & 15);
}
byte tobcd(byte v)
{
    return ((v / 10) << 4) | (v % 10);
}
int fromlongbcd(byte* p)
{
    return (frombcd(p[0]) << 16) | (frombcd(p[1]) << 8) | (frombcd(p[2]) << 0);
}
void tolongbcd(byte* p, int v)
{
    p[0] = tobcd((v >> 16) & 0xff);
    p[1] = tobcd((v >> 8) & 0xff);
    p[2] = tobcd((v >> 0) & 0xff);
}

static struct cd_toc* gettoc(struct cd_toc_head* th, int block)
{
    for (int i = th->first_track_offset; i < th->last_track_offset; i++)
    {
        struct cd_toc* t = &th->toc[i];
        if (block < t->paddress)
        {
            if (i == th->first_track_offset)
                return t;
            else
                return t - 1;
        }
    }
    return &th->toc[th->last_track_offset];
}

int isaudiotrack(struct cd_toc_head* th, int block)
{
    struct cd_toc* t = gettoc(th, block);
    if (!t)
        return 0;
    return (t->control & 0x0c) != 4;
}
int isdatatrack(struct cd_toc_head* th, int block)
{
    return !isaudiotrack(th, block);
}

static CDSlotType cdscsidevicetype[MAX_TOTAL_SCSI_DEVICES];

#ifdef _WIN32

extern struct device_functions devicefunc_win32_aspi;
extern struct device_functions devicefunc_win32_spti;
extern struct device_functions devicefunc_win32_ioctl;
extern struct device_functions devicefunc_cdimage;

static struct device_functions* devicetable[] =
{
    nullptr,
    &devicefunc_cdimage,
    &devicefunc_win32_ioctl,
    &devicefunc_win32_spti,
    &devicefunc_win32_aspi,
    nullptr
};
static int driver_installed[6];

static void install_driver(CDSlotType flags)
{
    for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        scsiemu[i] = false;
        device_func[i] = nullptr;
    }
    if (flags > 0)
    {
        device_func[0] = devicetable[flags];
        scsiemu[0] = true;
    }
    else
    {
        for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
        {
            scsiemu[i] = false;
            device_func[i] = nullptr;
            switch (cdscsidevicetype[i])
            {
                case cdslottype_Image:
                    device_func[i] = devicetable[cdslottype_Image];
                    scsiemu[i] = true;
                    break;
                case cdslottype_IOCTL:
                    device_func[i] = devicetable[cdslottype_IOCTL];
                    scsiemu[i] = true;
                    break;
                case cdslottype_SPTI:
                    if (g_curr_conf.win32_uaescsimode == UAESCSI_CDEMU)
                    {
                        device_func[i] = devicetable[cdslottype_IOCTL];
                        scsiemu[i] = true;
                    }
                    else
                    {
                        device_func[i] = devicetable[cdslottype_SPTI];
                    }
                    break;
                case cdslottype_ASPI:
                    device_func[i] = devicetable[cdslottype_ASPI];
                    break;
            }
        }
    }

    for (int j = 1; devicetable[j]; j++)
    {
        if (!driver_installed[j])
        {
            for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
            {
                if (device_func[i] == devicetable[j])
                {
                    int ok = device_func[i]->openbus(0);
                    driver_installed[j] = 1;

                    if (Logging::BLKDEV_DEBUG)
                        Logger::Write(L"%s driver installed, ok=%d\n", device_func[i]->name, ok);

                    break;
                }
            }
        }
    }
}
#endif

void blkdev_default_prefs(CConfiguration* p)
{
    for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        p->cdslots[i].name = "";
        p->cdslots[i].inuse = false;
        p->cdslots[i].type = cdslottype_Default;
        cdscsidevicetype[i] = cdslottype_Default;
    }
}

void blkdev_fix_prefs(CConfiguration* p)
{
    for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        cdscsidevicetype[i] = p->cdslots[i].type;

        if (!p->cdslots[i].inuse && (p->cdslots[i].name != "") && (p->cdslots[i].type != cdslottype_Disabled))
            p->cdslots[i].inuse = true;
    }

    //  blkdev_win32_aspi.cpp does not support multi units
    if (g_curr_conf.win32_uaescsimode >= UAESCSI_ASPI_FIRST)
    {
        for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
        {
            if (cdscsidevicetype[i] != cdslottype_Disabled)
                cdscsidevicetype[i] = cdslottype_ASPI;
        }
        return;
    }

    for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        if (cdscsidevicetype[i] != cdslottype_Default)
            continue;
        if (p->cdslots[i].inuse || (p->cdslots[i].name != ""))
        {
            const TCHAR* name = p->cdslots[i].name;
            if (_tcslen(name) == 3 && name[1] == ':' && name[2] == '\\')
            {
                if (g_curr_conf.scsi && (g_curr_conf.win32_uaescsimode == UAESCSI_SPTI || g_curr_conf.win32_uaescsimode == UAESCSI_SPTISCAN))
                    cdscsidevicetype[i] = cdslottype_SPTI;
                else
                    cdscsidevicetype[i] = cdslottype_IOCTL;
            }
            else
                cdscsidevicetype[i] = cdslottype_Image;
        }
        else if (g_curr_conf.scsi)
        {
            if (g_curr_conf.win32_uaescsimode == UAESCSI_CDEMU)
                cdscsidevicetype[i] = cdslottype_IOCTL;
            else if (g_curr_conf.win32_uaescsimode >= UAESCSI_ASPI_FIRST)
                cdscsidevicetype[i] = cdslottype_ASPI;
            else
                cdscsidevicetype[i] = cdslottype_SPTI;
        }
        else
            cdscsidevicetype[i] = cdslottype_IOCTL;
    }
}

static bool getsem(int unitnum, bool dowait)
{
    if (unitsem[unitnum] == nullptr)
        uae_sem_init(&unitsem[unitnum], 0, 1);
    bool gotit = false;
    if (dowait)
    {
        uae_sem_wait(&unitsem[unitnum]);
        gotit = true;
    }
    else
    {
        gotit = uae_sem_trywait(&unitsem[unitnum]) == 0;
    }
    if (gotit)
        unitsem_cnt[unitnum]++;
    if (unitsem_cnt[unitnum] > 1)
        Logger::Write(L"CD: unitsem%d acquire mismatch! cnt=%d\n", unitnum, unitsem_cnt[unitnum]);
    return gotit;
}
static bool getsem(int unitnum)
{
    return getsem(unitnum, false);
}
static void freesem(int unitnum)
{
    unitsem_cnt[unitnum]--;
    if (unitsem_cnt[unitnum] < 0)
        Logger::Write(L"CD: unitsem%d release mismatch! cnt=%d\n", unitnum, unitsem_cnt[unitnum]);
    uae_sem_post(&unitsem[unitnum]);
}
static void sys_command_close_internal(int unitnum)
{
    getsem(unitnum, true);
    waspaused[unitnum] = 0;
    if (openlist[unitnum] <= 0)
        Logger::Write(L"BUG unit %d close: opencnt=%d!\n", unitnum, openlist[unitnum]);
    if (device_func[unitnum])
    {
        device_func[unitnum]->closedev(unitnum);
        if (openlist[unitnum] > 0)
            openlist[unitnum]--;
    }
    freesem(unitnum);
    if (openlist[unitnum] == 0)
    {
        uae_sem_destroy(&unitsem[unitnum]);
        unitsem[unitnum] = nullptr;
    }
}

static int sys_command_open_internal(int unitnum, const TCHAR* ident, cd_standard_unit csu)
{
    int ret = 0;
    if (unitsem[unitnum] == nullptr)
        uae_sem_init(&unitsem[unitnum], 0, 1);
    getsem(unitnum, true);
    if (openlist[unitnum])
        Logger::Write(L"BUG unit %d open: opencnt=%d!\n", unitnum, openlist[unitnum]);
    if (device_func[unitnum])
    {
        ret = device_func[unitnum]->opendev(unitnum, ident, csu != CD_STANDARD_UNIT_DEFAULT);
        if (ret)
            openlist[unitnum]++;
    }
    freesem(unitnum);
    return ret;
}

static int getunitinfo(int unitnum, int drive, cd_standard_unit csu, int* isaudio)
{
    struct device_info di;
    if (sys_command_info(unitnum, &di, 0))
    {
        if (Logging::BLKDEV_DEBUG)
            Logger::Write(L"Scanning drive %s: ", di.label);

        if (di.media_inserted)
        {
            if (isaudiotrack(&di.toc, 0))
            {
                if (*isaudio == 0)
                    *isaudio = drive;
                Logger::Write(L"CDA");
            }
            byte buffer[2048];
            if (sys_command_cd_read(unitnum, buffer, 16, 1))
            {
                if (!memcmp(buffer + 8, "CDTV", 4) || !memcmp(buffer + 8, "CD32", 4) || !memcmp(buffer + 8, "COMM", 4))
                {
                    uint crc;
                    Logger::Write(L"CD32 or CDTV");
                    if (sys_command_cd_read(unitnum, buffer, 21, 1))
                    {
                        crc = get_crc32(buffer, sizeof buffer);
                        if (crc == 0xe56c340f)
                        {
                            Logger::Write(L" [CD32.TM]");
                            if (csu == CD_STANDARD_UNIT_CD32)
                            {
                                Logger::Write(L"\n");
                                return 1;
                            }
                        }
                    }
                    if (csu == CD_STANDARD_UNIT_CDTV || csu == CD_STANDARD_UNIT_CD32)
                    {
                        Logger::Write(L"\n");
                        return 1;
                    }
                }
            }
        }
        else
        {
            Logger::Write(L"no media");
        }
    }
    Logger::Write(L"\n");
    return 0;
}

static int get_standard_cd_unit2(cd_standard_unit csu)
{
    int unitnum = 0;
    int isaudio = 0;
    if ((g_curr_conf.cdslots[unitnum].name != "") || g_curr_conf.cdslots[unitnum].inuse)
    {
        if (g_curr_conf.cdslots[unitnum].name != "")
        {
            device_func_init(cdslottype_IOCTL);
            if (!sys_command_open_internal(unitnum, g_curr_conf.cdslots[unitnum].name, csu))
            {
                device_func_init(cdslottype_Image);
                if (!sys_command_open_internal(unitnum, g_curr_conf.cdslots[unitnum].name, csu))
                    goto fallback;
            }
        }
        else
        {
            goto fallback;
        }
        return unitnum;
    }
    device_func_init(cdslottype_IOCTL);
    for (int drive = 'C'; drive <= 'Z'; ++drive)
    {
        TCHAR vol[100];
        _stprintf(vol, L"%c:\\", drive);
        int drivetype = GetDriveType(vol);
        if (drivetype == DRIVE_CDROM)
        {
            if (sys_command_open_internal(unitnum, vol, csu))
            {
                if (getunitinfo(unitnum, drive, csu, &isaudio))
                    return unitnum;
                sys_command_close(unitnum);
            }
        }
    }
    if (isaudio)
    {
        TCHAR vol[100];
        _stprintf(vol, L"%c:\\", isaudio);
        if (sys_command_open_internal(unitnum, vol, csu))
            return unitnum;
    }
fallback:
    device_func_init(cdslottype_Image);
    if (!sys_command_open_internal(unitnum, L"", csu))
    {
        Logger::Write(L"image mounter failed to open as empty!?\n");
        return -1;
    }
    return unitnum;
}

int get_standard_cd_unit(cd_standard_unit csu)
{
    int unitnum = get_standard_cd_unit2(csu);
    if (unitnum < 0)
        return -1;
    // #ifdef RETROPLATFORM
    // rp_cd_device_enable(unitnum, true);
    // #endif
    delayed[unitnum] = 0;
    if (g_curr_conf.cdslots[unitnum].delayed)
    {
        delayed[unitnum] = PRE_INSERT_DELAY;
    }
    return unitnum;
}

void close_standard_cd_unit(int unitnum)
{
    sys_command_close(unitnum);
}

int sys_command_isopen(int unitnum)
{
    return openlist[unitnum];
}

int sys_command_open(int unitnum)
{
    waspaused[unitnum] = 0;
    int v = sys_command_open_internal(unitnum,
        (g_curr_conf.cdslots[unitnum].name != "") ? (LPCWSTR)g_curr_conf.cdslots[unitnum].name : nullptr, CD_STANDARD_UNIT_DEFAULT);
    if (!v)
        return 0;
    // #ifdef RETROPLATFORM
    // rp_cd_device_enable(unitnum, true);
    // #endif
    return v;
}

void sys_command_close(int unitnum)
{
    // #ifdef RETROPLATFORM
    // rp_cd_device_enable(unitnum, false);
    // #endif
    sys_command_close_internal(unitnum);
}

void blkdev_cd_change(int unitnum, const TCHAR* name)
{
    struct device_info di;
    sys_command_info(unitnum, &di, 1);
    // #ifdef RETROPLATFORM
    // rp_cd_image_change(unitnum, name);
    // #endif
}

void device_func_reset()
{
    for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        wasopen[i] = false;
        waspaused[i] = false;
        imagechangetime[i] = 0;
        cdimagefileinuse[i] = false;
        newimagefiles[i][0] = 0;
    }
}

int device_func_init(CDSlotType flags)
{
    blkdev_fix_prefs(&g_curr_conf);
    install_driver(flags);
    return 1;
}

void blkdev_entergui()
{
    for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        waspaused[i] = 0;
        struct device_info di;
        if (sys_command_info(i, &di, 1))
        {
            if (sys_command_cd_pause(i, 1) == 0)
                waspaused[i] = 1;
        }
    }
}
void blkdev_exitgui()
{
    for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
    {
        if (waspaused[i])
        {
            struct device_info di;
            if (sys_command_info(i, &di, 1))
            {
                sys_command_cd_pause(i, 0);
            }
        }
        waspaused[i] = 0;
    }
}

static void check_changes(int unitnum)
{
    bool changed = false;

    if (device_func[unitnum] == nullptr)
        return;

    if (delayed[unitnum])
    {
        delayed[unitnum]--;
        if (delayed[unitnum] == 0)
            Logger::Write(L"CD: startup delayed insert '%s'\n", (g_curr_conf.cdslots[unitnum].name != "") ? g_curr_conf.cdslots[unitnum].name : L"<EMPTY>");
        return;
    }

    if (_tcscmp(g_changed_conf.cdslots[unitnum].name, g_curr_conf.cdslots[unitnum].name) != 0)
        changed = true;
    if (!changed && (g_changed_conf.cdslots[unitnum].name == "") && g_changed_conf.cdslots[unitnum].inuse != g_curr_conf.cdslots[unitnum].inuse)
        changed = true;

    if (changed)
    {
        if (unitsem[unitnum])
            getsem(unitnum, true);
        cdimagefileinuse[unitnum] = g_changed_conf.cdslots[unitnum].inuse;
        _tcscpy(newimagefiles[unitnum], g_changed_conf.cdslots[unitnum].name);
        g_changed_conf.cdslots[unitnum].name = "";
        g_curr_conf.cdslots[unitnum].name = "";
        g_curr_conf.cdslots[unitnum].inuse = g_changed_conf.cdslots[unitnum].inuse;
        int pollmode = 0;
        imagechangetime[unitnum] = 3 * 50;
        struct device_info di;
        device_func[unitnum]->info(unitnum, &di, 0);
        wasopen[unitnum] = di.open;
        if (wasopen[unitnum])
        {
            device_func[unitnum]->closedev(unitnum);
            if (g_curr_conf.scsi)
            {
                scsi_do_disk_change(unitnum, 0, &pollmode);
                if (pollmode)
                    imagechangetime[unitnum] = 8 * 50;
            }
        }
        Logger::Write(L"CD: eject (%s)\n", pollmode ? L"slow" : L"fast");
        // #ifdef RETROPLATFORM
        // rp_cd_image_change(unitnum, nullptr);
        // #endif
        if (unitsem[unitnum])
            freesem(unitnum);
    }
    if (imagechangetime[unitnum] == 0)
        return;
    imagechangetime[unitnum]--;
    if (imagechangetime[unitnum] > 0)
        return;
    if (unitsem[unitnum])
        getsem(unitnum, true);
    g_curr_conf.cdslots[unitnum].name = newimagefiles[unitnum];
    g_changed_conf.cdslots[unitnum].name = newimagefiles[unitnum];
    g_curr_conf.cdslots[unitnum].inuse = cdimagefileinuse[unitnum];
    g_changed_conf.cdslots[unitnum].inuse = cdimagefileinuse[unitnum];
    newimagefiles[unitnum][0] = 0;
    Logger::Write(L"CD: delayed insert '%s'\n", (g_curr_conf.cdslots[unitnum].name != "") ? g_curr_conf.cdslots[unitnum].name : L"<EMPTY>");
    device_func_init(cdslottype_Default);
    if (wasopen[unitnum])
    {
        if (!device_func[unitnum]->opendev(unitnum, g_curr_conf.cdslots[unitnum].name, 0))
        {
            Logger::Write(L"-> device open failed\n");
            wasopen[unitnum] = 0;
        }
    }
    if (g_curr_conf.scsi)
    {
        struct device_info di;
        device_func[unitnum]->info(unitnum, &di, 0);
        int pollmode;
        scsi_do_disk_change(unitnum, 1, &pollmode);
    }
    // #ifdef RETROPLATFORM
    // rp_cd_image_change(unitnum, g_curr_conf.cdslots[unitnum].name);
    // #endif
    if (unitsem[unitnum])
        freesem(unitnum);

    config_changed = 1;
}

void blkdev_vsync()
{
    for (int i = 0; i < MAX_TOTAL_SCSI_DEVICES; i++)
        check_changes(i);
}

static int do_scsi(int unitnum, byte* cmd, int cmdlen)
{
    byte* p = device_func[unitnum]->exec_out(unitnum, cmd, cmdlen);
    return p != nullptr;
}

static int do_scsi(int unitnum, byte* cmd, int cmdlen, byte* out, int outsize)
{
    byte* p = device_func[unitnum]->exec_in(unitnum, cmd, cmdlen, &outsize);
    if (p)
        memcpy(out, p, outsize);
    return p != nullptr;
}

static int failunit(int unitnum)
{
    if (unitnum < 0 || unitnum >= MAX_TOTAL_SCSI_DEVICES)
        return 1;
    if (device_func[unitnum] == nullptr)
        return 1;
    return 0;
}

static int audiostatus(int unitnum)
{
    if (!getsem(unitnum))
        return 0;
    byte cmd[10] = { 0x42, 2, 0x40, 1, 0, 0, 0, (byte)(DEVICE_SCSI_BUFSIZE >> 8), (byte)(DEVICE_SCSI_BUFSIZE & 0xff), 0 };
    byte* p = device_func[unitnum]->exec_in(unitnum, cmd, sizeof(cmd), 0);
    freesem(unitnum);
    if (!p)
        return 0;
    return p[1];
}

/* pause/unpause CD audio */
int sys_command_cd_pause(int unitnum, int paused)
{
    if (failunit(unitnum))
        return -1;
    if (!getsem(unitnum))
        return 0;
    int v;
    if (device_func[unitnum]->pause == nullptr)
    {
        int as = audiostatus(unitnum);
        byte cmd[10] = { 0x4b, 0, 0, 0, 0, 0, 0, 0, paused ? 0 : 1, 0 };
        do_scsi(unitnum, cmd, sizeof cmd);
        v = as == AUDIO_STATUS_PAUSED;
    }
    else
    {
        v = device_func[unitnum]->pause(unitnum, paused);
    }
    freesem(unitnum);
    return v;
}

/* stop CD audio */
void sys_command_cd_stop(int unitnum)
{
    if (failunit(unitnum))
        return;
    if (!getsem(unitnum))
        return;
    if (device_func[unitnum]->stop == nullptr)
    {
        /*int as =*/ audiostatus(unitnum);
        byte cmd[6] = { 0x4e, 0, 0, 0, 0, 0 };
        do_scsi(unitnum, cmd, sizeof cmd);
    }
    else
    {
        device_func[unitnum]->stop(unitnum);
    }
    freesem(unitnum);
}

/* play CD audio */
int sys_command_cd_play(int unitnum, int startlsn, int endlsn, int scan)
{
    int v;
    if (failunit(unitnum))
        return 0;
    if (!getsem(unitnum))
        return 0;
    play_end_pos[unitnum] = endlsn;
    if (device_func[unitnum]->play == nullptr)
    {
        byte cmd[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
        int startmsf = lsn2msf(startlsn);
        int endmsf = lsn2msf(endlsn);
        cmd[0] = 0x47;
        cmd[3] = (byte)(startmsf >> 16);
        cmd[4] = (byte)(startmsf >> 8);
        cmd[5] = (byte)(startmsf >> 0);
        cmd[6] = (byte)(endmsf >> 16);
        cmd[7] = (byte)(endmsf >> 8);
        cmd[8] = (byte)(endmsf >> 0);
        v = do_scsi(unitnum, cmd, sizeof cmd) ? 0 : 1;
    }
    else
    {
        v = device_func[unitnum]->play(unitnum, startlsn, endlsn, scan, nullptr, nullptr);
    }
    freesem(unitnum);
    return v;
}

/* play CD audio with subchannels */
int sys_command_cd_play(int unitnum, int startlsn, int endlsn, int scan, play_status_callback statusfunc, play_subchannel_callback subfunc)
{
    int v;
    if (failunit(unitnum))
        return 0;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->play == nullptr)
        v = sys_command_cd_play(unitnum, startlsn, endlsn, scan);
    else
        v = device_func[unitnum]->play(unitnum, startlsn, endlsn, scan, statusfunc, subfunc);
    freesem(unitnum);
    return v;
}

/* set CD audio volume */
uint sys_command_cd_volume(int unitnum, ushort volume_left, ushort volume_right)
{
    int v;
    if (failunit(unitnum))
        return 0;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->volume == nullptr)
        v = -1;
    else
        v = device_func[unitnum]->volume(unitnum, volume_left, volume_right);
    freesem(unitnum);
    return v;
}

/* read qcode */
int sys_command_cd_qcode(int unitnum, byte* buf)
{
    int v;
    if (failunit(unitnum))
        return 0;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->qcode == nullptr)
    {
        byte cmd[10] = { 0x42, 2, 0x40, 1, 0, 0, 0, (byte)(SUBQ_SIZE >> 8), (byte)(SUBQ_SIZE & 0xff), 0 };
        v = do_scsi(unitnum, cmd, sizeof cmd, buf, SUBQ_SIZE);
    }
    else
    {
        v = device_func[unitnum]->qcode(unitnum, buf, -1);
    }
    freesem(unitnum);
    return v;
}

/* read table of contents */
int sys_command_cd_toc(int unitnum, struct cd_toc_head* toc)
{
    int v;
    if (failunit(unitnum))
        return 0;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->toc == nullptr)
    {
        byte buf[4 + 8 * 103];
        int size = sizeof buf;
        byte cmd[10] = { 0x43, 0, 2, 0, 0, 0, 0, (byte)(size >> 8), (byte)(size & 0xff), 0 };
        if (do_scsi(unitnum, cmd, sizeof cmd, buf, size))
        {
            //  toc parse to do
            v = 0;
        }
        v = 0;
    }
    else
    {
        v = device_func[unitnum]->toc(unitnum, toc);
    }
    freesem(unitnum);
    return v;
}

/* read one cd sector */
int sys_command_cd_read(int unitnum, byte* data, int block, int size)
{
    int v;
    if (failunit(unitnum))
        return 0;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->read == nullptr)
    {
        byte cmd[12] = { 0xbe, 0, block >> 24, block >> 16, block >> 8, block >> 0, size >> 16, size >> 8, size >> 0, 0x10, 0, 0 };
        v = do_scsi(unitnum, cmd, sizeof cmd, data, size * 2048);
    }
    else
    {
        v = device_func[unitnum]->read(unitnum, data, block, size);
    }
    freesem(unitnum);
    return v;
}
int sys_command_cd_rawread(int unitnum, byte* data, int block, int size, int sectorsize)
{
    int v;
    if (failunit(unitnum))
        return -1;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->rawread == nullptr)
    {
        byte cmd[12] = { 0xbe, 0, block >> 24, block >> 16, block >> 8, block >> 0, size >> 16, size >> 8, size >> 0, 0x10, 0, 0 };
        v = do_scsi(unitnum, cmd, sizeof cmd, data, size * sectorsize);
    }
    else
    {
        v = device_func[unitnum]->rawread(unitnum, data, block, size, sectorsize, 0xffffffff);
    }
    freesem(unitnum);
    return v;
}
int sys_command_cd_rawread(int unitnum, byte* data, int block, int size, int sectorsize, byte sectortype, byte scsicmd9, byte subs)
{
    int v;
    if (failunit(unitnum))
        return -1;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->rawread == nullptr)
    {
        byte cmd[12] = { 0xbe, 0, block >> 24, block >> 16, block >> 8, block >> 0, size >> 16, size >> 8, size >> 0, 0x10, 0, 0 };
        v = do_scsi(unitnum, cmd, sizeof cmd, data, size * sectorsize);
    }
    else
    {
        v = device_func[unitnum]->rawread(unitnum, data, block, size, sectorsize, (sectortype << 16) | (scsicmd9 << 8) | subs);
    }
    freesem(unitnum);
    return v;
}

/* read block */
int sys_command_read(int unitnum, byte* data, int block, int size)
{
    int v;
    if (failunit(unitnum))
        return 0;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->read == nullptr)
    {
        byte cmd[12] = { 0xa8, 0, 0, 0, 0, 0, size >> 24, size >> 16, size >> 8, size >> 0, 0, 0 };
        cmd[2] = (byte)(block >> 24);
        cmd[3] = (byte)(block >> 16);
        cmd[4] = (byte)(block >> 8);
        cmd[5] = (byte)(block >> 0);
        v = do_scsi(unitnum, cmd, sizeof cmd, data, size * 2048);
    }
    else
    {
        v = device_func[unitnum]->read(unitnum, data, block, size);
    }
    freesem(unitnum);
    return v;
}

/* write block */
int sys_command_write(int unitnum, byte* data, int offset, int size)
{
    int v;
    if (failunit(unitnum))
        return 0;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum]->write == nullptr)
    {
        v = 0;
    }
    else
    {
        v = device_func[unitnum]->write(unitnum, data, offset, size);
    }
    freesem(unitnum);
    return v;
}

int sys_command_ismedia(int unitnum, int quick)
{
    int v;
    if (failunit(unitnum))
        return -1;
    if (delayed[unitnum])
        return 0;
    if (!getsem(unitnum))
        return 0;
    if (device_func[unitnum] == nullptr)
    {
        byte cmd[6] = { 0, 0, 0, 0, 0, 0 };
        v = do_scsi(unitnum, cmd, sizeof cmd);
    }
    else
    {
        v = device_func[unitnum]->ismedia(unitnum, quick);
    }
    freesem(unitnum);
    return v;
}

struct device_info* sys_command_info(int unitnum, struct device_info* di, int quick)
{
    if (failunit(unitnum))
        return nullptr;
    if (!getsem(unitnum))
        return 0;
    struct device_info* di2 = device_func[unitnum]->info(unitnum, di, quick);
    if (di2 && delayed[unitnum])
        di2->media_inserted = 0;
    freesem(unitnum);
    return di2;
}

#define MODE_SELECT_6 0x15
#define MODE_SENSE_6 0x1a
#define MODE_SELECT_10 0x55
#define MODE_SENSE_10 0x5a

void scsi_atapi_fixup_pre(byte* scsi_cmd, int* len, byte** datap, int* datalenp, int* parm)
{
    byte cmd, * p, * data = *datap;
    int l, datalen = *datalenp;

    *parm = 0;
    cmd = scsi_cmd[0];
    if (cmd != MODE_SELECT_6 && cmd != MODE_SENSE_6)
        return;
    l = scsi_cmd[4];
    if (l > 4)
        l += 4;
    scsi_cmd[7] = l >> 8;
    scsi_cmd[8] = l;
    if (cmd == MODE_SELECT_6)
    {
        scsi_cmd[0] = MODE_SELECT_10;
        scsi_cmd[9] = scsi_cmd[5];
        scsi_cmd[2] = scsi_cmd[3] = scsi_cmd[4] = scsi_cmd[5] = scsi_cmd[6] = 0;
        *len = 10;
        p = xmalloc(byte, 8 + datalen + 4);
        if (datalen > 4)
            memcpy(p + 8, data + 4, datalen - 4);
        p[0] = 0;
        p[1] = data[0];
        p[2] = data[1];
        p[3] = data[2];
        p[4] = p[5] = p[6] = 0;
        p[7] = data[3];
        if (l > 8)
            datalen += 4;
        *parm = MODE_SELECT_10;
        *datap = p;
    }
    else
    {
        scsi_cmd[0] = MODE_SENSE_10;
        scsi_cmd[9] = scsi_cmd[5];
        scsi_cmd[3] = scsi_cmd[4] = scsi_cmd[5] = scsi_cmd[6] = 0;
        if (l > 8)
            datalen += 4;
        *datap = xmalloc(byte, datalen);
        *len = 10;
        *parm = MODE_SENSE_10;
    }
    *datalenp = datalen;
}

void scsi_atapi_fixup_post(byte* scsi_cmd, int len, byte* olddata, byte* data, int* datalenp, int parm)
{
    int datalen = *datalenp;
    if (!data || !datalen)
        return;
    if (parm == MODE_SENSE_10)
    {
        olddata[0] = data[1];
        olddata[1] = data[2];
        olddata[2] = data[3];
        olddata[3] = data[7];
        datalen -= 4;
        if (datalen > 4)
            memcpy(olddata + 4, data + 8, datalen - 4);
        *datalenp = datalen;
    }
}

static void scsi_atapi_fixup_inquiry(struct amigascsi* as)
{
    byte* scsi_data = as->data;
    uint scsi_len = as->len;
    byte* scsi_cmd = as->cmd;
    byte cmd;

    cmd = scsi_cmd[0];
    /* CDROM INQUIRY: most Amiga programs expect ANSI version == 2
     * (ATAPI normally responds with zero)
     */
    if (cmd == 0x12 && scsi_len > 2 && scsi_data)
    {
        byte per = scsi_data[0];
        byte b = scsi_data[2];
        /* CDROM and ANSI version == 0 ? */
        if ((per & 31) == 5 && (b & 7) == 0)
        {
            b |= 2;
            scsi_data[2] = b;
        }
    }
}

void scsi_log_before(byte* cdb, int cdblen, byte* data, int datalen)
{
    int i;
    for (i = 0; i < cdblen; i++)
    {
        Logger::Write(L"%s%02X", i > 0 ? L"." : L"", cdb[i]);
    }
    Logger::Write(L"\n");
    if (data)
    {
        Logger::Write(L"DATAOUT: %d\n", datalen);
        for (i = 0; i < datalen && i < 100; i++)
            Logger::Write(L"%s%02X", i > 0 ? L"." : L"", data[i]);
        if (datalen > 0)
            Logger::Write(L"\n");
    }
}

void scsi_log_after(byte* data, int datalen, byte* sense, int senselen)
{
    int i;
    Logger::Write(L"DATAIN: %d\n", datalen);
    for (i = 0; i < datalen && i < 100 && data; i++)
        Logger::Write(L"%s%02X", i > 0 ? L"." : L"", data[i]);
    if (data && datalen > 0)
        Logger::Write(L"\n");
    if (senselen > 0)
    {
        Logger::Write(L"SENSE: %d,", senselen);
        for (i = 0; i < senselen && i < 32; i++)
        {
            Logger::Write(L"%s%02X", i > 0 ? L"." : L"", sense[i]);
        }
        Logger::Write(L"\n");
    }
}

static bool nodisk(struct device_info* di)
{
    return di->media_inserted == 0;
}
static ulong64 cmd_readx(int unitnum, byte* dataptr, int offset, int len)
{
    if (!getsem(unitnum))
        return 0;
    int v = device_func[unitnum]->read(unitnum, dataptr, offset, len);
    freesem(unitnum);
    if (v)
        return len;
    return 0;
}

static void wl(byte* p, int v)
{
    p[0] = v >> 24;
    p[1] = v >> 16;
    p[2] = v >> 8;
    p[3] = v;
}
static void ww(byte* p, int v)
{
    p[0] = v >> 8;
    p[1] = v;
}
static int rl(byte* p)
{
    return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | (p[3]);
}
static int rw(byte* p)
{
    return (p[0] << 8) | (p[1]);
}

static void stopplay(int unitnum)
{
    sys_command_cd_stop(unitnum);
}

static int addtocentry(byte** dstp, int* len, int point, int newpoint, int msf, byte* head, struct cd_toc_head* th)
{
    byte* dst = *dstp;

    for (int i = 0; i < th->points; i++)
    {
        struct cd_toc* t = &th->toc[i];
        if (t->point == point)
        {
            if (*len < 8)
                return 0;
            int addr = t->paddress;
            if (msf)
                addr = lsn2msf(addr);
            dst[0] = 0;
            dst[1] = (t->adr << 4) | t->control;
            dst[2] = newpoint >= 0 ? newpoint : point;
            dst[3] = 0;
            dst[4] = addr >> 24;
            dst[5] = addr >> 16;
            dst[6] = addr >> 8;
            dst[7] = addr >> 0;

            if (point >= 1 && point <= 99)
            {
                if (head[2] == 0)
                    head[2] = point;
                head[3] = point;
            }

            *len -= 8;
            *dstp = dst + 8;
            return 1;
        }
    }
    return -1;
}

static int scsiemudrv(int unitnum, byte* cmd)
{
    if (failunit(unitnum))
        return -1;
    if (!getsem(unitnum))
        return 0;
    int v = 0;
    if (device_func[unitnum]->scsiemu)
        v = device_func[unitnum]->scsiemu(unitnum, cmd);
    freesem(unitnum);
    return v;
}

static int scsi_read_cd(int unitnum, byte* cmd, byte* data, struct device_info* di)
{
    int msf = cmd[0] == 0xb9;
    int start = msf ? msf2lsn(rl(cmd + 2) & 0x00ffffff) : rl(cmd + 2);
    int len = rl(cmd + 5) & 0x00ffffff;
    if (msf)
    {
        int end = msf2lsn(len);
        len = end - start;
        if (len < 0)
            return -1;
    }
    int subs = cmd[10] & 7;
    if (len == 0)
        return 0;
    return sys_command_cd_rawread(unitnum, data, start, len, 0, (cmd[1] >> 2) & 7, cmd[9], subs);
}

static int scsi_emulate(int unitnum, byte* cmdbuf, int scsi_cmd_len,
    byte* scsi_data, int* data_len, byte* r, int* reply_len, byte* s, int* sense_len)
{
    ulong64 len, offset;
    int lr = 0, ls = 0;
    int scsi_len = -1;
    int status = 0;
    struct device_info di;
    byte cmd = cmdbuf[0];

    *reply_len = *sense_len = 0;
    memset(r, 0, 256);
    memset(s, 0, 256);

    sys_command_info(unitnum, &di, 1);

    if (cmdbuf[0] == 0)   /* TEST UNIT READY */
    {
        if (nodisk(&di))
            goto nodisk;
        scsi_len = 0;
        goto end;
    }
    if (Logging::SCSI_EMU_DEBUG)
        Logger::Write(L"SCSIEMU %d: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X CMDLEN=%d DATA=%08X LEN=%d\n", unitnum,
            cmdbuf[0], cmdbuf[1], cmdbuf[2], cmdbuf[3], cmdbuf[4], cmdbuf[5], cmdbuf[6],
            cmdbuf[7], cmdbuf[8], cmdbuf[9], cmdbuf[10], cmdbuf[11],
            scsi_cmd_len, scsi_data, *data_len);
    switch (cmdbuf[0])
    {
        case 0x12: /* INQUIRY */
        {
            if ((cmdbuf[1] & 1) || cmdbuf[2] != 0)
                goto err;
            len = cmdbuf[4];
            if (cmdbuf[1] >> 5)
                goto err;
            r[0] = 5; // CDROM
            r[1] |= 0x80; // removable
            r[2] = 2; /* supports SCSI-2 */
            r[3] = 2; /* response data format */
            r[4] = 32; /* additional length */
            r[7] = 0x20; /* 16 bit bus */
            scsi_len = lr = len < 36 ? (uint)len : 36;
            r[2] = 2;
            r[3] = 2;
            char* s = Unicode::ua(di.vendorid);
            memcpy(r + 8, s, strlen(s));
            free(s);
            s = Unicode::ua(di.productid);
            memcpy(r + 16, s, strlen(s));
            free(s);
            s = Unicode::ua(di.revision);
            memcpy(r + 32, s, strlen(s));
            free(s);
            for (int i = 8; i < 36; i++)
            {
                if (r[i] == 0)
                    r[i] = 32;
            }
        }
        break;
        case 0xbe: // READ CD
        case 0xb9: // READ CD MSF
            if (nodisk(&di))
                goto nodisk;
            scsi_len = scsi_read_cd(unitnum, cmdbuf, scsi_data, &di);
            if (scsi_len == -2)
                goto notdatatrack;
            if (scsi_len == -1)
                goto errreq;
            break;
        case 0x55: // MODE SELECT(10)
        case 0x15: // MODE SELECT(6)
        {
            byte* p;
            p = scsi_data + 4;
            if (cmdbuf[0] == 0x55)
                p += 4;
            int pcode = p[0] & 0x3f;
            if (pcode == 14) // CD audio control
            {
                ushort vol_left = (p[9] << 7) | (p[9] >> 1);
                ushort vol_right = (p[11] << 7) | (p[11] >> 1);
                sys_command_cd_volume(unitnum, vol_left, vol_right);
                scsi_len = 0;
            }
            else
            {
                goto errreq;
            }
        }
        break;
        case 0x5a: // MODE SENSE(10)
        case 0x1a: /* MODE SENSE(6) */
        {
            byte* p;
            int pc = cmdbuf[2] >> 6;
            int pcode = cmdbuf[2] & 0x3f;
            int dbd = cmdbuf[1] & 8;
            if (cmdbuf[0] == 0x5a)
                dbd = 1;
            if (Logging::SCSI_EMU_DEBUG)
                Logger::Write(L"MODE SENSE PC=%d CODE=%d DBD=%d\n", pc, pcode, dbd);
            p = r;
            if (cmdbuf[0] == 0x5a)
            {
                p[0] = 8 - 1;
                p[1] = 0;
                p[2] = 0;
                p[3] = 0;
                p[4] = 0;
                p[5] = 0;
                p[6] = 0;
                p[7] = 0;
                p += 8;
            }
            else
            {
                p[0] = 4 - 1;
                p[1] = 0;
                p[2] = 0;
                p[3] = 0;
                p += 4;
            }
            if (!dbd)
            {
                if (nodisk(&di))
                    goto nodisk;
                uint blocks = di.sectorspertrack * di.cylinders * di.trackspercylinder;
                p[-1] = 8;
                wl(p + 0, blocks);
                wl(p + 4, di.bytespersector);
                p += 8;
            }
            if (pcode == 0)
            {
                p[0] = 0;
                p[1] = 0;
                p[2] = 0x20;
                p[3] = 0;
                r[0] += 4;
            }
            else if (pcode == 3)
            {
                if (nodisk(&di))
                    goto nodisk;
                p[0] = 3;
                p[1] = 24;
                p[3] = 1;
                p[10] = di.trackspercylinder >> 8;
                p[11] = di.trackspercylinder;
                p[12] = di.bytespersector >> 8;
                p[13] = di.bytespersector;
                p[15] = 1; // interleave
                p[20] = 0x80;
                r[0] += p[1];
            }
            else if (pcode == 4)
            {
                if (nodisk(&di))
                    goto nodisk;
                p[0] = 4;
                wl(p + 1, di.cylinders);
                p[1] = 24;
                p[5] = 1;
                wl(p + 13, di.cylinders);
                ww(p + 20, 0);
                r[0] += p[1];
            }
            else if (pcode == 14) // CD audio control
            {
                uint vol = sys_command_cd_volume(unitnum, 0xffff, 0xffff);
                p[0] = 0x0e;
                p[1] = 0x0e;
                p[2] = 1;
                p[3] = 4;
                p[6] = 0;
                p[7] = 75;
                p[8] = 1;
                p[9] = pc == 0 ? (vol >> 7) & 0xff : 0xff;
                p[10] = 2;
                p[11] = pc == 0 ? (vol >> (16 + 7)) & 0xff : 0xff;
                r[0] += p[1];
            }
            else
            {
                goto err;
            }
            r[0] += r[3];
            scsi_len = lr = r[0] + 1;
        }
        break;
        case 0x1d: /* SEND DIAGNOSTICS */
            scsi_len = 0;
            break;
        case 0x25: /* READ_CAPACITY */
        {
            int pmi = cmdbuf[8] & 1;
            uint lba = (cmdbuf[2] << 24) | (cmdbuf[3] << 16) | (cmdbuf[4] << 8) | cmdbuf[5];
            int cyl, cylsec, head, tracksec;
            if (nodisk(&di))
                goto nodisk;
            uint blocks = di.sectorspertrack * di.cylinders * di.trackspercylinder;
            cyl = di.cylinders;
            head = 1;
            cylsec = tracksec = di.trackspercylinder;
            if (pmi == 0 && lba != 0)
                goto errreq;
            if (pmi)
            {
                lba += tracksec * head;
                lba /= tracksec * head;
                lba *= tracksec * head;
                if (lba > blocks)
                    lba = blocks;
                blocks = lba;
            }
            wl(r, blocks);
            wl(r + 4, di.bytespersector);
            scsi_len = lr = 8;
        }
        break;
        case 0x08: /* READ (6) */
        {
            if (nodisk(&di))
                goto nodisk;
            stopplay(unitnum);
            offset = ((cmdbuf[1] & 31) << 16) | (cmdbuf[2] << 8) | cmdbuf[3];
            struct cd_toc* t = gettoc(&di.toc, offset);
            if ((t->control & 0x0c) == 0x04)
            {
                len = cmdbuf[4];
                if (!len)
                    len = 256;
                scsi_len = (uint)cmd_readx(unitnum, scsi_data, offset, len) * di.bytespersector;
            }
            else
            {
                goto notdatatrack;
            }
        }
        break;
        case 0x0a: /* WRITE (6) */
            goto readprot;
        case 0x28: /* READ (10) */
        {
            if (nodisk(&di))
                goto nodisk;
            stopplay(unitnum);
            offset = rl(cmdbuf + 2);
            struct cd_toc* t = gettoc(&di.toc, offset);
            if ((t->control & 0x0c) == 0x04)
            {
                len = rl(cmdbuf + 7 - 2) & 0xffff;
                scsi_len = cmd_readx(unitnum, scsi_data, offset, len) * di.bytespersector;
            }
            else
            {
                goto notdatatrack;
            }
        }
        break;
        case 0x2a: /* WRITE (10) */
            goto readprot;
        case 0xa8: /* READ (12) */
        {
            if (nodisk(&di))
                goto nodisk;
            stopplay(unitnum);
            offset = rl(cmdbuf + 2);
            struct cd_toc* t = gettoc(&di.toc, offset);
            if ((t->control & 0x0c) == 0x04)
            {
                len = rl(cmdbuf + 6);
                scsi_len = (uint)cmd_readx(unitnum, scsi_data, offset, len) * di.bytespersector;
            }
            else
            {
                goto notdatatrack;
            }
        }
        break;
        case 0xaa: /* WRITE (12) */
            goto readprot;
        case 0x43: // READ TOC
        {
            if (nodisk(&di))
                goto nodisk;
            byte* p = scsi_data;
            int strack = cmdbuf[6];
            int msf = cmdbuf[1] & 2;
            int format = cmdbuf[2] & 7;
            if (format >= 3)
                goto errreq;
            int maxlen = (cmdbuf[7] << 8) | cmdbuf[8];
            struct cd_toc_head ttoc;
            if (!sys_command_cd_toc(unitnum, &ttoc))
                goto readerr;
            struct cd_toc_head* toc = &ttoc;
            if (maxlen < 4)
                goto errreq;
            if (format == 1)
            {
                p[0] = 0;
                p[1] = 8;
                p[2] = 1;
                p[3] = 1;
                p[4] = 0;
                p[5] = (toc->toc[0].adr << 4) | toc->toc[0].control;
                p[6] = toc->first_track;
                p[7] = 0;
                if (msf)
                    wl(p + 8, lsn2msf(toc->toc[0].address));
                else
                    wl(p + 8, toc->toc[0].address);
                scsi_len = 12;
            }
            else if (format == 2 || format == 0)
            {
                if (format == 2 && !msf)
                    goto errreq;
                if (strack == 0)
                    strack = toc->first_track;
                if (format == 0 && strack >= 100 && strack != 0xaa)
                    goto errreq;
                byte* p2 = p + 4;
                p[2] = 0;
                p[3] = 0;
                maxlen -= 4;
                if (format == 2)
                {
                    if (!addtocentry(&p2, &maxlen, 0xa0, -1, msf, p, toc))
                        goto errreq;
                    if (!addtocentry(&p2, &maxlen, 0xa1, -1, msf, p, toc))
                        goto errreq;
                    if (!addtocentry(&p2, &maxlen, 0xa2, -1, msf, p, toc))
                        goto errreq;
                }
                while (strack < 100)
                {
                    if (!addtocentry(&p2, &maxlen, strack, -1, msf, p, toc))
                        goto errreq;
                    strack++;
                }
                if (!addtocentry(&p2, &maxlen, 0xa2, 0xaa, msf, p, toc))
                    goto errreq;
                int tlen = p2 - (p + 4);
                p[0] = tlen >> 8;
                p[1] = tlen >> 0;
                scsi_len = tlen + 4;
            }
        }
        break;
        case 0x42: // READ SUB-CHANNEL
        {
            int msf = cmdbuf[1] & 2;
            int subq = cmdbuf[2] & 0x40;
            int format = cmdbuf[3];
            // int track = cmdbuf[6];
            int len = rw(cmdbuf + 7);
            byte buf[SUBQ_SIZE] = { 0 };

            if (nodisk(&di))
                goto nodisk;
            sys_command_cd_qcode(unitnum, buf);
            if (len < 4)
                goto errreq;
            scsi_len = 4;
            scsi_data[0] = 0;
            scsi_data[1] = buf[1];
            if (subq && format == 1)
            {
                if (len < 4 + 12)
                    goto errreq;
                scsi_data[2] = 0;
                scsi_data[3] = 12;
                scsi_len += 12;
                scsi_data[4] = 1;
                scsi_data[5] = (buf[4 + 0] << 4) | (buf[4 + 0] >> 4);
                scsi_data[6] = frombcd(buf[4 + 1]);  // track
                scsi_data[7] = frombcd(buf[4 + 2]);  // index
                int reladdr = fromlongbcd(&buf[4 + 3]);
                int absaddr = fromlongbcd(&buf[4 + 7]);
                if (!msf)
                {
                    reladdr = msf2lsn(reladdr);
                    absaddr = msf2lsn(absaddr);
                }
                wl(scsi_data + 8, absaddr);
                wl(scsi_data + 12, reladdr);
            }
            else
            {
                scsi_data[2] = 0;
                scsi_data[3] = 0;
            }
        }
        break;
        case 0x1b: // START/STOP
            sys_command_cd_stop(unitnum);
            scsiemudrv(unitnum, cmdbuf);
            scsi_len = 0;
            break;
        case 0x1e: // PREVENT/ALLOW MEDIA REMOVAL
            //  do nothing
            scsi_len = 0;
            break;
        case 0x4e: // STOP PLAY/SCAN
            if (nodisk(&di))
                goto nodisk;
            sys_command_cd_stop(unitnum);
            scsi_len = 0;
            break;
        case 0xba: // SCAN
        {
            if (nodisk(&di))
                goto nodisk;
            struct cd_toc_head ttoc;
            if (!sys_command_cd_toc(unitnum, &ttoc))
                goto readerr;
            struct cd_toc_head* toc = &ttoc;
            int scan = (cmdbuf[1] & 0x10) ? -1 : 1;
            int start = rl(cmdbuf + 1) & 0x00ffffff;
            int end = scan > 0 ? toc->lastaddress : toc->toc[toc->first_track_offset].paddress;
            int type = cmdbuf[9] >> 6;
            if (type == 1)
                start = lsn2msf(start);
            if (type == 3)
                goto errreq;
            if (type == 2)
            {
                if (toc->first_track_offset + start >= toc->last_track_offset)
                    goto errreq;
                start = toc->toc[toc->first_track_offset + start].paddress;
            }
            sys_command_cd_pause(unitnum, 0);
            sys_command_cd_play(unitnum, start, end, scan);
            scsi_len = 0;
        }
        break;
        case 0x48: // PLAY AUDIO TRACK/INDEX
        {
            if (nodisk(&di))
                goto nodisk;
            int strack = cmdbuf[4];
            int etrack = cmdbuf[7];
            struct cd_toc_head ttoc;
            if (!sys_command_cd_toc(unitnum, &ttoc))
                goto readerr;
            struct cd_toc_head* toc = &ttoc;
            if (strack < toc->first_track || strack > toc->last_track ||
                etrack < toc->first_track || etrack > toc->last_track ||
                strack > etrack)
                goto errreq;
            int start = toc->toc[toc->first_track_offset + strack - 1].paddress;
            int end = etrack == toc->last_track ? toc->lastaddress : toc->toc[toc->first_track_offset + etrack - 1 + 1].paddress;
            sys_command_cd_pause(unitnum, 0);
            if (!sys_command_cd_play(unitnum, start, end, 0))
                goto notdatatrack;
            scsi_len = 0;
        }
        break;
        case 0x49: // PLAY AUDIO TRACK RELATIVE (10)
        case 0xa9: // PLAY AUDIO TRACK RELATIVE (12)
        {
            if (nodisk(&di))
                goto nodisk;
            int len = cmd == 0xa9 ? rl(cmdbuf + 6) : rw(cmdbuf + 7);
            int track = cmd == 0xa9 ? cmdbuf[10] : cmdbuf[6];
            if (track < di.toc.first_track || track > di.toc.last_track)
                goto errreq;
            int start = di.toc.toc[di.toc.first_track_offset + track - 1].paddress;
            int rel = rl(cmdbuf + 2);
            start += rel;
            int end = start + len;
            if (end > di.toc.lastaddress)
                end = di.toc.lastaddress;
            if (len > 0)
            {
                sys_command_cd_pause(unitnum, 0);
                if (!sys_command_cd_play(unitnum, start, start + len, 0))
                    goto notdatatrack;
            }
            scsi_len = 0;
        }
        break;
        case 0x47: // PLAY AUDIO MSF
        {
            if (nodisk(&di))
                goto nodisk;
            int start = rl(cmdbuf + 2) & 0x00ffffff;
            if (start == 0x00ffffff)
            {
                byte buf[SUBQ_SIZE] = { 0 };
                sys_command_cd_qcode(unitnum, buf);
                start = fromlongbcd(buf + 4 + 7);
            }
            int end = msf2lsn(rl(cmdbuf + 5) & 0x00ffffff);
            if (end > di.toc.lastaddress)
                end = di.toc.lastaddress;
            start = msf2lsn(start);
            if (start > end)
                goto errreq;
            if (start < end)
                sys_command_cd_pause(unitnum, 0);
            if (!sys_command_cd_play(unitnum, start, end, 0))
                goto notdatatrack;
            scsi_len = 0;
        }
        break;
        case 0x45: // PLAY AUDIO (10)
        case 0xa5: // PLAY AUDIO (12)
        {
            if (nodisk(&di))
                goto nodisk;
            int start = rl(cmdbuf + 2);
            int len;
            if (cmd = 0xa5)
                len = rl(cmdbuf + 6);
            else
                len = rw(cmdbuf + 7);
            if (len > 0)
            {
                if (start == -1)
                {
                    byte buf[SUBQ_SIZE] = { 0 };
                    sys_command_cd_qcode(unitnum, buf);
                    start = msf2lsn(fromlongbcd(buf + 4 + 7));
                }
                int end = start + len;
                if (end > di.toc.lastaddress)
                    end = di.toc.lastaddress;
                sys_command_cd_pause(unitnum, 0);
                if (!sys_command_cd_play(unitnum, start, end, 0))
                    goto notdatatrack;
            }
            scsi_len = 0;
        }
        break;
        case 0xbc: // PLAY CD
        {
            if (nodisk(&di))
                goto nodisk;
            int start = -1;
            int end = -1;
            if (cmdbuf[1] & 2)
            {
                start = msf2lsn(rl(cmdbuf + 2) & 0x00ffffff);
                end = msf2lsn(rl(cmdbuf + 5) & 0x00ffffff);
            }
            else
            {
                start = rl(cmdbuf + 2);
                end = start + rl(cmdbuf + 6);
            }
            if (end > di.toc.lastaddress)
                end = di.toc.lastaddress;
            if (start > end)
                goto errreq;
            if (start < end)
            {
                sys_command_cd_pause(unitnum, 0);
                if (!sys_command_cd_play(unitnum, start, end, 0))
                    goto notdatatrack;
            }
        }
        break;
        case 0x4b: // PAUSE/RESUME
        {
            if (nodisk(&di))
                goto nodisk;
            byte buf[SUBQ_SIZE] = { 0 };
            int resume = cmdbuf[8] & 1;
            sys_command_cd_qcode(unitnum, buf);
            if (buf[1] != AUDIO_STATUS_IN_PROGRESS && buf[1] != AUDIO_STATUS_PAUSED)
                goto errreq;
            sys_command_cd_pause(unitnum, resume ? 0 : 1);
            scsi_len = 0;
        }
        break;
readprot:
            status = 2; /* CHECK CONDITION */
            s[0] = 0x70;
            s[2] = 7; /* DATA PROTECT */
            s[12] = 0x27; /* WRITE PROTECTED */
            ls = 12;
            break;
nodisk:
            status = 2; /* CHECK CONDITION */
            s[0] = 0x70;
            s[2] = 2; /* NOT READY */
            s[12] = 0x3A; /* MEDIUM NOT PRESENT */
            ls = 12;
            break;
readerr:
            status = 2; /* CHECK CONDITION */
            s[0] = 0x70;
            s[2] = 2; /* NOT READY */
            s[12] = 0x11; /* UNRECOVERED READ ERROR */
            ls = 12;
            break;
notdatatrack:
            status = 2;
            s[0] = 0x70;
            s[2] = 5;
            s[12] = 0x64; /* ILLEGAL MODE FOR THIS TRACK */
            ls = 12;
            break;

        default:
err:
            Logger::Write(L"CDEMU: unsupported scsi command 0x%02X\n", cmdbuf[0]);
errreq:
            lr = -1;
            status = 2; /* CHECK CONDITION */
            s[0] = 0x70;
            s[2] = 5; /* ILLEGAL REQUEST */
            s[12] = 0x24; /* ILLEGAL FIELD IN CDB */
            ls = 12;
            break;
    }
end:
    *data_len = scsi_len;
    *reply_len = lr;
    *sense_len = ls;
    if (cmdbuf[0] && Logging::SCSI_EMU_DEBUG)
        Logger::Write(L"-> DATAOUT=%d ST=%d SENSELEN=%d\n", scsi_len, status, ls);
    return status;
}

static int execscsicmd_direct(int unitnum, struct amigascsi* as)
{
    int sactual = 0;
    int io_error = 0;
    byte* scsi_datap, * scsi_datap_org;
    // uint scsi_cmd_len_orig = as->cmd_len;
    byte cmd[16] = { 0 };
    byte replydata[256];
    int datalen = as->len;
    int senselen = as->sense_len;
    int replylen = 0;

    memcpy(cmd, as->cmd, as->cmd_len);
    scsi_datap = scsi_datap_org = as->len ? as->data : 0;
    if (as->sense_len > 32)
        as->sense_len = 32;

    as->status = scsi_emulate(unitnum, cmd, as->cmd_len, scsi_datap, &datalen, replydata, &replylen, as->sensedata, &senselen);

    as->cmdactual = as->status != 0 ? 0 : as->cmd_len; /* fake scsi_CmdActual */
    if (as->status)
    {
        io_error = IOERR_BadStatus;
        as->sense_len = senselen;
        as->actual = 0; /* scsi_Actual */
    }
    else
    {
        int i;
        if (replylen > 0)
        {
            for (i = 0; i < replylen; i++)
                scsi_datap[i] = replydata[i];
            datalen = replylen;
        }
        for (i = 0; i < as->sense_len; i++)
            as->sensedata[i] = 0;
        sactual = 0;
        if (datalen < 0)
        {
            io_error = IOERR_NotSpecified;
            as->actual = 0; /* scsi_Actual */
        }
        else
        {
            as->len = datalen;
            io_error = 0;
            as->actual = as->len; /* scsi_Actual */
        }
    }
    as->sactual = sactual;

    return io_error;
}

int sys_command_scsi_direct_native(int unitnum, struct amigascsi* as)
{
    if (scsiemu[unitnum])
    {
        return execscsicmd_direct(unitnum, as);
    }
    else
    {
        if (!device_func[unitnum]->exec_direct)
            return -1;
    }
    int ret = device_func[unitnum]->exec_direct(unitnum, as);
    if (!ret && device_func[unitnum]->isatapi(unitnum))
        scsi_atapi_fixup_inquiry(as);
    return ret;
}

int sys_command_scsi_direct(int unitnum, uae_ptr acmd)
{
    int ret, i;
    struct amigascsi as;
    uae_ptr ap;
    addrbank* bank;

    ap = get_long(acmd + 0);
    as.len = get_long(acmd + 4);

    bank = &get_mem_bank(ap);
    if (!bank || !bank->check(ap, as.len))
        return IOERR_BADADDRESS;
    as.data = bank->xlateaddr(ap);

    ap = get_long(acmd + 12);
    as.cmd_len = get_word(acmd + 16);
    if (as.cmd_len > sizeof as.cmd)
        return IOERR_BADLENGTH;
    for (i = 0; i < as.cmd_len; i++)
        as.cmd[i] = get_byte(ap++);
    while (i < sizeof as.cmd)
        as.cmd[i++] = 0;
    as.flags = get_byte(acmd + 20);
    as.sense_len = get_word(acmd + 26);

    ret = sys_command_scsi_direct_native(unitnum, &as);

    put_long(acmd + 8, as.actual);
    put_word(acmd + 18, as.cmdactual);
    put_byte(acmd + 21, as.status);
    put_word(acmd + 28, as.sactual);

    ap = get_long(acmd + 22);
    for (i = 0; i < as.sactual; i++)
        put_byte(ap, as.sensedata[i]);

    return ret;
}

#ifdef SAVESTATE

byte* save_cd(int num, int* len)
{
    byte* dstbak, * dst;

    memset(play_qcode[num], 0, SUBQ_SIZE);
    if (!g_curr_conf.cdslots[num].inuse || num >= MAX_TOTAL_SCSI_DEVICES)
        return nullptr;
    if (!g_curr_conf.cs_cd32cd && !g_curr_conf.cs_cdtvcd && !g_curr_conf.scsi)
        return nullptr;
    dstbak = dst = xmalloc(byte, 4 + 256 + 4 + 4);
    save_u32(4 | 8);
    save_path(g_curr_conf.cdslots[num].name, SAVESTATE_PATH_CD);
    save_u32(g_curr_conf.cdslots[num].type);
    save_u32(0);
    save_u32(0);
    sys_command_cd_qcode(num, play_qcode[num]);
    for (int i = 0; i < SUBQ_SIZE; i++)
        save_u8(play_qcode[num][i]);
    save_u32(play_end_pos[num]);
    *len = dst - dstbak;
    return dstbak;
}

byte* restore_cd(int num, byte* src)
{
    uint flags;
    TCHAR* s;

    if (num >= MAX_TOTAL_SCSI_DEVICES)
        return nullptr;
    flags = restore_u32();
    s = restore_path(SAVESTATE_PATH_CD);
    int type = restore_u32();
    restore_u32();
    if (flags & 4)
    {
        g_changed_conf.cdslots[num].name = s;
        g_curr_conf.cdslots[num].name = s;
        g_changed_conf.cdslots[num].type = (CDSlotType)type;
        g_curr_conf.cdslots[num].type = (CDSlotType)type;
    }
    if (flags & 8)
    {
        restore_u32();
        for (int i = 0; i < SUBQ_SIZE; i++)
            play_qcode[num][i] = restore_u8();
        play_end_pos[num] = restore_u32();
    }
    return src;
}

#endif